diff options
Diffstat (limited to 'app/[lng]')
| -rw-r--r-- | app/[lng]/evcp/(evcp)/po-rfq/page.tsx | 86 | ||||
| -rw-r--r-- | app/[lng]/partners/(partners)/rfq-all/[id]/page.tsx | 80 | ||||
| -rw-r--r-- | app/[lng]/partners/(partners)/rfq-all/page.tsx | 171 |
3 files changed, 337 insertions, 0 deletions
diff --git a/app/[lng]/evcp/(evcp)/po-rfq/page.tsx b/app/[lng]/evcp/(evcp)/po-rfq/page.tsx new file mode 100644 index 00000000..dfaa7708 --- /dev/null +++ b/app/[lng]/evcp/(evcp)/po-rfq/page.tsx @@ -0,0 +1,86 @@ +import { Suspense } from "react" +import { getPORfqs } from "@/lib/procurement-rfqs/services" +import { searchParamsCache } from "@/lib/procurement-rfqs/validations" +import { Shell } from "@/components/shell" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import RFQContainer from "@/components/po-rfq/po-rfq-container" + +interface RfqPageProps { + searchParams: Promise<{ [key: string]: string | string[] | undefined }>; + title?: string; + description?: string; +} + +export default async function RfqPage({ + searchParams, + title = "발주용 견적", + description = "SAP으로부터 전송된 발주용 견적을 관리할 수 있습니다.", +}: RfqPageProps) { + // searchParams를 await하여 resolve + const resolvedSearchParams = await searchParams; + + // 서버 액션: RFQ 데이터 가져오기 + async function fetchRfqData(params: any) { + "use server" + + try { + // URL 파라미터를 추출하고 필요한 형식으로 변환 + const parsedParams = searchParamsCache.parse(params); + + // RFQ 데이터 가져오기 + const data = await getPORfqs(parsedParams) + + return data + } catch (error) { + console.error("RFQ 데이터 조회 오류:", error) + // 에러 발생 시 빈 결과 반환 + return { data: [], pageCount: 0, total: 0 } + } + } + + // 현재 resolvedSearchParams를 파싱하여 초기 데이터 로드 + const initialParams = { + page: resolvedSearchParams.page?.toString() || "1", + perPage: resolvedSearchParams.perPage?.toString() || "10", + sort: resolvedSearchParams.sort?.toString() || JSON.stringify([{ id: "updatedAt", desc: true }]), + filters: resolvedSearchParams.filters?.toString() || null, + joinOperator: resolvedSearchParams.joinOperator?.toString() || "and", + basicFilters: resolvedSearchParams.basicFilters?.toString() || null, + basicJoinOperator: resolvedSearchParams.basicJoinOperator?.toString() || "and", + search: resolvedSearchParams.search?.toString() || "", + } + + // 초기 데이터 로드 + const initialData = await fetchRfqData(initialParams) + + return ( + <Shell className="gap-2"> + <div className="flex items-center justify-between space-y-2"> + <div className="flex items-center justify-between space-y-2"> + <div> + <h2 className="text-2xl font-bold tracking-tight"> + {title} + </h2> + </div> + </div> + </div> + + <Suspense + fallback={ + <DataTableSkeleton + columnCount={6} + searchableColumnCount={1} + filterableColumnCount={2} + cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem", "8rem"]} + shrinkZero + /> + } + > + <RFQContainer + initialData={initialData} + fetchData={fetchRfqData} + /> + </Suspense> + </Shell> + ) +}
\ No newline at end of file diff --git a/app/[lng]/partners/(partners)/rfq-all/[id]/page.tsx b/app/[lng]/partners/(partners)/rfq-all/[id]/page.tsx new file mode 100644 index 00000000..c8858704 --- /dev/null +++ b/app/[lng]/partners/(partners)/rfq-all/[id]/page.tsx @@ -0,0 +1,80 @@ +// app/vendor/quotations/[id]/page.tsx - 견적 응답 페이지 +import { Metadata } from "next" +import { notFound } from "next/navigation" +import db from "@/db/db"; +import { eq } from "drizzle-orm" +import { procurementVendorQuotations } from "@/db/schema" +import { getServerSession } from "next-auth/next" +import { authOptions } from "@/app/api/auth/[...nextauth]/route" +import VendorQuotationEditor from "@/lib/procurement-rfqs/vendor-response/quotation-editor"; + + +interface PageProps { + params: { + id: string + } +} + +export async function generateMetadata({ params }: PageProps): Promise<Metadata> { + return { + title: "견적서 응답", + description: "RFQ에 대한 견적서 작성 및 제출", + } +} + +export default async function VendorQuotationPage({ params }: PageProps) { + const quotationId = parseInt(params.id) + + if (isNaN(quotationId)) { + notFound() + } + + // 인증 확인 + const session = await getServerSession(authOptions); + + if (!session?.user) { + return ( + <div className="flex h-full items-center justify-center"> + <div className="text-center"> + <h2 className="text-xl font-bold">로그인이 필요합니다</h2> + <p className="mt-2 text-muted-foreground">견적서 응답을 위해 로그인해주세요.</p> + </div> + </div> + ) + } + + // 견적서 정보 가져오기 + const quotation = await db.query.procurementVendorQuotations.findFirst({ + where: eq(procurementVendorQuotations.id, quotationId), + with: { + rfq: true, // 관계 설정 필요 + vendor: true, // 관계 설정 필요 + items: true, // 관계 설정 필요 + } + }) + + if (!quotation) { + notFound() + } + + // 벤더 권한 확인 (필요한 경우) + const isAuthorized = session.user.domain === "partners" && + session.user.companyId === quotation.vendorId + + if (!isAuthorized) { + return ( + <div className="flex h-full items-center justify-center"> + <div className="text-center"> + <h2 className="text-xl font-bold">접근 권한이 없습니다</h2> + <p className="mt-2 text-muted-foreground">이 견적서에 대한 권한이 없습니다.</p> + </div> + </div> + ) + } + + return ( + <div className="container py-8"> + <VendorQuotationEditor quotation={quotation} /> + </div> + ) +}
\ No newline at end of file diff --git a/app/[lng]/partners/(partners)/rfq-all/page.tsx b/app/[lng]/partners/(partners)/rfq-all/page.tsx new file mode 100644 index 00000000..e7dccb02 --- /dev/null +++ b/app/[lng]/partners/(partners)/rfq-all/page.tsx @@ -0,0 +1,171 @@ +// app/vendor/quotations/page.tsx +import * as React from "react"; +import Link from "next/link"; +import { Metadata } from "next"; +import { getServerSession } from "next-auth/next"; +import { authOptions } from "@/app/api/auth/[...nextauth]/route"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { LogIn } from "lucide-react"; +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton"; +import { Shell } from "@/components/shell"; +import { getValidFilters } from "@/lib/data-table"; +import { type SearchParams } from "@/types/table"; +import { searchParamsVendorRfqCache } from "@/lib/procurement-rfqs/validations"; +import { getQuotationStatusCounts, getVendorQuotations } from "@/lib/procurement-rfqs/services"; +import { VendorQuotationsTable } from "@/lib/procurement-rfqs/vendor-response/table/vendor-quotations-table"; + +export const metadata: Metadata = { + title: "견적 목록", + description: "진행 중인 견적서 목록", +}; + +interface IndexPageProps { + searchParams: Promise<SearchParams> +} + + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsVendorRfqCache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + // 인증 확인 + const session = await getServerSession(authOptions); + + // 로그인 확인 + if (!session || !session.user) { + return ( + <Shell className="gap-6"> + <div className="flex items-center justify-between"> + <div> + <h2 className="text-2xl font-bold tracking-tight"> + 견적 목록 + </h2> + <p className="text-muted-foreground"> + 진행 중인 견적서 목록을 확인하고 관리합니다. + </p> + </div> + </div> + + <div className="flex flex-col items-center justify-center py-12 text-center"> + <div className="rounded-lg border border-dashed p-10 shadow-sm"> + <h3 className="mb-2 text-xl font-semibold">로그인이 필요합니다</h3> + <p className="mb-6 text-muted-foreground"> + 견적서를 확인하려면 먼저 로그인하세요. + </p> + <Button size="lg" asChild> + <Link href="/partners?callbackUrl=/vendor/quotations"> + <LogIn className="mr-2 h-4 w-4" /> + 로그인하기 + </Link> + </Button> + </div> + </div> + </Shell> + ); + } + + // 벤더 ID 확인 + const vendorId = session.user.companyId ? String(session.user.companyId) : "0"; + + // 벤더 권한 확인 + if (session.user.domain !== "partners") { + return ( + <Shell className="gap-6"> + <div className="flex items-center justify-between"> + <div> + <h2 className="text-2xl font-bold tracking-tight"> + 접근 권한 없음 + </h2> + </div> + </div> + <div className="flex flex-col items-center justify-center py-12 text-center"> + <div className="rounded-lg border border-dashed p-10 shadow-sm"> + <h3 className="mb-2 text-xl font-semibold">벤더 계정이 필요합니다</h3> + <p className="mb-6 text-muted-foreground"> + 벤더 계정으로 로그인해주세요. + </p> + </div> + </div> + </Shell> + ); + } + + // 데이터 가져오기 + const quotationsPromise = getVendorQuotations({ + ...search, + filters: validFilters + }, vendorId); + + // 상태별 개수 가져오기 + const statusCountsPromise = getQuotationStatusCounts(vendorId); + + // 모든 프로미스 병렬 실행 + const promises = Promise.all([quotationsPromise]); + const statusCounts = await statusCountsPromise; + + return ( + <Shell className="gap-6"> + <div className="flex justify-between items-center"> + <div> + <h2 className="text-2xl font-bold tracking-tight">견적 목록</h2> + <p className="text-muted-foreground"> + 진행 중인 견적서 목록을 확인하고 관리합니다. + </p> + </div> + </div> + + <div className="grid gap-4 md:grid-cols-4"> + <Card> + <CardHeader className="py-4"> + <CardTitle className="text-base">전체 견적</CardTitle> + </CardHeader> + <CardContent> + <div className="text-2xl font-bold"> + {Object.values(statusCounts).reduce((sum, count) => sum + count, 0)}건 + </div> + </CardContent> + </Card> + <Card> + <CardHeader className="py-4"> + <CardTitle className="text-base">작성 중</CardTitle> + </CardHeader> + <CardContent> + <div className="text-2xl font-bold">{statusCounts.Draft || 0}건</div> + </CardContent> + </Card> + <Card> + <CardHeader className="py-4"> + <CardTitle className="text-base">제출됨</CardTitle> + </CardHeader> + <CardContent> + <div className="text-2xl font-bold"> + {(statusCounts.Submitted || 0) + (statusCounts.Revised || 0)}건 + </div> + </CardContent> + </Card> + <Card> + <CardHeader className="py-4"> + <CardTitle className="text-base">승인됨</CardTitle> + </CardHeader> + <CardContent> + <div className="text-2xl font-bold">{statusCounts.Accepted || 0}건</div> + </CardContent> + </Card> + </div> + + <React.Suspense + fallback={ + <DataTableSkeleton + columnCount={7} + searchableColumnCount={2} + filterableColumnCount={3} + cellWidths={["10rem", "10rem", "8rem", "10rem", "10rem", "10rem", "8rem"]} + /> + } + > + <VendorQuotationsTable promises={promises} /> + </React.Suspense> + </Shell> + ); +}
\ No newline at end of file |
